﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Xml.Linq;

namespace SilverMap
{
	/// <summary>
	/// Represents a tile map control that utilizes the WriteableBitmap to render the tiles.
	/// </summary>
	[TemplatePart(Name = PART_ViewPort, Type = typeof(FrameworkElement))]
	[TemplatePart(Name = PART_Surface, Type = typeof(Image))]
	public class UIMap : Control
	{
		#region Constants
		private const string PART_ViewPort = "PART_ViewPort";
		private const string PART_Surface = "PART_Surface";
		#endregion

		#region Fields
		private FrameworkElement _viewPortElement;
		private Image _surfaceElement;

		private bool _initialized = false;
		private bool _requiresRedraw = false;

		private WriteableBitmap _buffer;
		private Point _center = new Point();
		private Size _viewPort = new Size();

		private double _halfX = 0;
		private double _halfY = 0;

		private int _layerStart = 0;
		private int _layerEnd = 0;

		private MapLayerCollection _layers = new MapLayerCollection();
		#endregion

		#region Events
		/// <summary>
		/// An event that is raised when the map control has completed initialization.
		/// </summary>
		public event EventHandler Initialized = delegate { };
		#endregion

		#region Properties
		/// <summary>
		/// Gets the current amount of time that has elapsed since the last draw. 
		/// </summary>
		protected TimeSpan ElapsedTime { get; private set; }

		/// <summary>
		/// Gets the collection of IMapItem instances for the current UIMap.
		/// </summary>
		public MapItemCollection Items { get; private set; }

		/// <summary>
		/// Gets or sets a value indicating whether or not the tile grid should be displayed.
		/// </summary>
		public bool IsTileGridVisible
		{
			get { return (bool)GetValue(IsTileGridVisibleProperty); }
			set { SetValue(IsTileGridVisibleProperty, value); }
		}
		/// <summary>
		/// Identifies the IsTileGridVisible attached property.
		/// </summary>
		public static readonly DependencyProperty IsTileGridVisibleProperty = DependencyProperty.Register("IsTileGridVisible", typeof(bool), typeof(UIMap), new PropertyMetadata(false, new PropertyChangedCallback(UIMap.OnMapPropertyChanged)));
            
		/// <summary>
		/// Gets or sets the size of the grid.
		/// </summary>
		public Size GridSize
		{
			get { return (Size)GetValue(GridSizeProperty); }
			set { SetValue(GridSizeProperty, value); }
		}
		/// <summary>
		/// Identifies the GridSize property.
		/// </summary>
		public static readonly DependencyProperty GridSizeProperty = DependencyProperty.Register("GridSize", typeof(Size), typeof(UIMap), new PropertyMetadata(new Size(64, 64), new PropertyChangedCallback(UIMap.OnMapPropertyChanged)));

		/// <summary>
		/// Gets the highest layer currently available.
		/// </summary>
		public int LayerUpperBound
		{
			get { return _layerEnd; }
		}

		/// <summary>
		/// Gets the current center of the map.
		/// </summary>
		public Point Center
		{
			get { return _center; }
		}

		/// <summary>
		/// Gets the current offset of the world map in relation to the visible screen area.
		/// </summary>
		public Point Offset
		{
			get { return _center.Subtract(_halfX, _halfY); }
		}

		/// <summary>
		/// Gets or sets the scale of the entire map, does not effect individual tile scale values but scales the map control itself.
		/// </summary>
		public double MapScale
		{
			get { return (double)GetValue(MapScaleProperty); }
			set { SetValue(MapScaleProperty, value); }
		}
		/// <summary>
		/// Identifies the MapScale attached property.
		/// </summary>
		public static readonly DependencyProperty MapScaleProperty = DependencyProperty.Register("MapScale", typeof(double), typeof(UIMap), new PropertyMetadata(1.0, new PropertyChangedCallback(UIMap.OnMapPropertyChanged)));

		/// <summary>
		/// Gets or sets the background color of the WriteableBitmap.
		/// </summary>
		public Color MapColor
		{
			get { return (Color)GetValue(MapColorProperty); }
			set { SetValue(MapColorProperty, value); }
		}
		/// <summary>
		/// Identitifes the MapColor attached property.
		/// </summary>
		public static readonly DependencyProperty MapColorProperty = DependencyProperty.Register("MapColor", typeof(Color), typeof(UIMap), new PropertyMetadata(Colors.Black));

		/// <summary>
		/// Gets or sets the color of the grid lines and map item borders.
		/// </summary>
		public Color BorderColor
		{
			get { return (Color)GetValue(BorderColorProperty); }
			set { SetValue(BorderColorProperty, value); }
		}
		/// <summary>
		/// Identitifes the ItemBorderColor attached property.
		/// </summary>
		public static readonly DependencyProperty BorderColorProperty = DependencyProperty.Register("BorderColor", typeof(Color), typeof(UIMap), new PropertyMetadata(Color.FromArgb(20, Colors.Yellow.R, Colors.Yellow.G, Colors.Yellow.B), new PropertyChangedCallback(UIMap.OnMapPropertyChanged)));
            
		/// <summary>
		/// Gets or sets the currently selected map item.
		/// </summary>
		public IMapItem SelectedItem
		{
			get { return (IMapItem)GetValue(SelectedItemProperty); }
			set { SetValue(SelectedItemProperty, value); }
		}
		/// <summary>
		/// Identitifes the SelectedItem attached property.
		/// </summary>
		public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.Register("SelectedItem", typeof(IMapItem), typeof(UIMap), new PropertyMetadata(null));
				
		#endregion	

		#region Static Members
		private static void OnMapPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
		{
			var map = obj as UIMap;
			if (map == null)
				return;

			map.Draw(map.ElapsedTime);
		}

		#endregion

		#region ctor
		/// <summary>
		/// Initializes a new instance of the SilverMap.UIMap control.
		/// </summary>
		public UIMap()
		{
			this.DefaultStyleKey = typeof(UIMap);
			this.SizeChanged += OnSizeChanged;

			this.ElapsedTime = TimeSpan.Zero;

			this.Items = new MapItemCollection();
			this.Items.CollectionChanged += OnMapItemCollectionChanged;
		}
		#endregion

		#region Event Handlers
		public override void OnApplyTemplate()
		{
			base.OnApplyTemplate();

			_viewPortElement = GetTemplateChild(PART_ViewPort) as FrameworkElement;
			_surfaceElement = GetTemplateChild(PART_Surface) as Image;

			if (this.Background == null)
			{
				// Set an almost transparent background so mouse events will be fired on the map surface,
				// regardless of whether or not any tiles have been rendered.
				this.Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0));
			}

			Initialize();
		}

		protected void OnSizeChanged(object sender, SizeChangedEventArgs e)
		{
			Initialize();
			Draw(ElapsedTime);
		}

		private void OnMapItemCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
		{
			Draw(ElapsedTime);
		}
		#endregion

		#region Initialize
		private void Initialize()
		{
			// Using the dispatcher ensures that ActualWidth and ActualHeight of the UIMap control will be set.
			this.Dispatcher.BeginInvoke(() =>
			{
				try
				{
					_viewPort = new Size(this.ActualWidth, this.ActualHeight);

					_buffer = new WriteableBitmap((int)_viewPort.Width, (int)_viewPort.Height);
					_buffer.Clear(MapColor);

					_halfX = _viewPort.Width * 0.5;
					_halfY = _viewPort.Height * 0.5;

					_center = new Point(_halfX, _halfY);

					_surfaceElement.Source = _buffer;
					_surfaceElement.Width = _viewPort.Width;
					_surfaceElement.Height = _viewPort.Height;

					_initialized = true;

					if (_requiresRedraw)
					{
						Draw(ElapsedTime);
						_requiresRedraw = false;
					}
					Initialized(this, EventArgs.Empty);
				}
				catch (Exception ex)
				{
					throw new SilverMapException(ex.Message, ex);
				}
			});
		}
		#endregion

		#region Load
		/// <summary>
		/// Loads the map from a file stream.
		/// </summary>
		/// <param name="stream">The file stream containing the map items.</param>
		/// <param name="getTileCallback">The callback method used to set the Tile property of each item.</param>
		public void Load(Stream stream, GetTileCallback getTileCallback)
		{
			if (stream == null)
				throw new ArgumentNullException("stream");

			try
			{
				var rootElement = XElement.Load(stream);

				// ==========================================================================
				// Convert to new map format, from 0.2 format.
				// ==========================================================================
				if (rootElement.Name == "items")
				{
					var oldItemsElement = rootElement;
					rootElement = new XElement("map");
					rootElement.Add(oldItemsElement);
				}
				// ==========================================================================

				// Map Color

				// Map Scale

				// Grid Size

				// Background Layer Details
				var layersElement = rootElement.Element("layers");
				if (layersElement != null)
				{
					_layers.Load(layersElement);
				}

				// Items
				var itemsElement = rootElement.Element("items");
				if (itemsElement != null)
				{
					var items = new MapItemCollection();
					items.Load(itemsElement, getTileCallback);
					Load(items);
				}
			}
			catch (Exception ex)
			{
				throw new SilverMapException(ex.Message, ex);
			}
		}

		private void Load(IEnumerable<IMapItem> items)
		{
			this.Items.CollectionChanged -= OnMapItemCollectionChanged;
			this.Clear();
			foreach (var item in items)
			{
				this.Items.Add(item);
			}
			this.Items.CollectionChanged += OnMapItemCollectionChanged;

			RecalculateLayers();

			Draw(ElapsedTime);
		}
		#endregion

		#region Save
		/// <summary>
		/// Saves the current map to the specified stream.
		/// </summary>
		/// <param name="stream">The stream in which to save the map data.</param>
		public void Save(Stream stream)
		{
			if (stream == null)
				throw new ArgumentNullException("stream");

			var rootElement = new XElement("map");

			// Map Color

			// Map Scale

			// Grid Size

			// Background Layer Details
			var layersElement = new XElement("layers");
			rootElement.Add(layersElement);
			_layers.Save(layersElement);

			// Items
			var itemsElement = new XElement("items");
			rootElement.Add(itemsElement);
			this.Items.Save(itemsElement);

			rootElement.Save(stream);
		}
		#endregion

		#region Layer Methods
		/// <summary>
		/// Adds a new layer to the collection.
		/// </summary>
		/// <returns>The newly added layer index.</returns>
		public int AddLayer()
		{
			var layer = _layers.Add();
			RecalculateLayers();
			return layer.Index;
		}

		/// <summary>
		/// Recalculates the current layers.
		/// </summary>
		public void RecalculateLayers()
		{
			if (_layers.Count > 0)
			{
				_layerStart = _layers.Min(i => i.Key);
				_layerEnd = _layers.Max(i => i.Key);	
			}
			else
			{
				_layerStart = this.Items.Min(i => i.Layer);
				_layerEnd = this.Items.Max(i => i.Layer);	
				for (int i = _layerStart; i <= _layerEnd; i++)
				{
					if (!_layers.ContainsKey(i))
						_layers.Add();
				}
			}
		}

		/// <summary>
		/// Gets the scroll rate value for the specified layer.
		/// </summary>
		/// <param name="layer">The layer for which to retrieve the scroll rate.</param>
		/// <returns>The scroll rate value of the layer.</returns>
		public double GetScrollRate(int layer)
		{
			MapLayer result;
			if (_layers.TryGetValue(layer, out result))
			{
				return result.ScrollRate;
			}
			return 0.0;
		}

		/// <summary>
		/// Sets the scroll rate for the specified layer.
		/// </summary>
		/// <param name="layer">The layer for which to set the scroll rate.</param>
		/// <param name="scrollRate">The value of the scroll rate.</param>
		public void SetScrollRate(int layer, double scrollRate)
		{
			MapLayer result;
			if (_layers.TryGetValue(layer, out result))
			{
				result.ScrollRate = scrollRate;
			}
		}

		/// <summary>
		/// Gets the max z-index for the specified layer.
		/// </summary>
		/// <param name="layer">The layer to retrieve the z-index.</param>
		/// <returns>The highest z_index for the specified layer.</returns>
		public double GetMaxZIndex(int layer)
		{
			var items = this.Items.Where(i => i.Layer == layer);
			if (items.Count() > 0)
				return items.Max(i => i.Z);
			return 0.0;
		}
		#endregion

		#region Selection Methods
		/// <summary>
		/// Attempts to find an IMapItem instance within the collection where the specified position is within the item boundary.
		/// If the item is found a border is draw around the item.
		/// </summary>
		/// <param name="position">The position at which to search for a tile.</param>
		/// <param name="layer">The current layer to search.</param>
		/// <returns>The first IMapItem instance found or null if no items were found on the specified layer.</returns>
		public IMapItem SelectItem(Point position, int layer)
		{
			var item = this.Items.Where(i => (position.X >= i.X && position.X <= (i.X + i.Width))
				&& (position.Y >= i.Y && (position.Y <= i.Y + i.Height))
				&& i.Layer == layer).OrderByDescending(i => i.Z).FirstOrDefault();

			if (item != null)
			{
				this.SelectedItem = item;
				Draw(ElapsedTime);
			}
			return item;
		}

		/// <summary>
		/// Clears the selected item.
		/// </summary>
		public void ClearSelection()
		{
			this.SelectedItem = null;
			Draw(ElapsedTime);
		}
		#endregion

		#region Clear
		/// <summary>
		/// Clears the map, resetting the items collection.
		/// </summary>
		public void Clear()
		{
			this.Items.Clear();
			if (_buffer != null)
				_buffer.Clear(MapColor);
		}
		#endregion

		#region Camera Methods
		/// <summary>
		/// Sets the camera view centered on the specified position.
		/// </summary>
		/// <param name="position">The center position at which to set the camera.</param>
		public virtual void SetCamera(Point position)
		{
			_center = position;
			Draw(ElapsedTime);
		}

		/// <summary>
		/// Sets the camera view centered on the specified position.
		/// </summary>
		/// <param name="x">The x coordinate at which to position the camera.</param>
		/// <param name="y">The y coordinate at which to position the camera.</param>
		public void SetCamera(double x, double y)
		{
			SetCamera(new Point(x, y));
		}
		#endregion

		#region Drawing Methods
		/// <summary>
		/// Draws the current map items onto the WriteableBitmap surface.
		/// </summary>
		/// <param name="elapsedTime">Elapsed time since last draw operation.</param>
		/// <exception cref="SilverMap.SilverMapException"/>
		public virtual void Draw(TimeSpan elapsedTime)
		{
			this.ElapsedTime = elapsedTime;

			if (!_initialized)
			{
				_requiresRedraw = true;
				return;
			}

			if (_surfaceElement == null)
				throw new SilverMapException(SR.SurfaceImageNotFound);

			try
			{
				// Create a boundary within the world map the same size as the visible screen.
				var bounds = new Rect(_center.X - _halfX, _center.Y - _halfY, _viewPort.Width, _viewPort.Height);

				_buffer.Clear(MapColor);

				for (int layer = _layerStart; layer <= _layerEnd; layer++)
				{
					DrawLayer(elapsedTime, bounds, layer);
				}

				if (IsTileGridVisible)
				{
					DrawGrid(bounds);
				}

				_buffer.Invalidate();
			}
			catch (Exception ex)
			{
				throw new SilverMapException(ex.Message, ex);
			}
		}

		/// <summary>
		/// Draws the map items for the specified layer, where the map items are within the specified world bounds.
		/// </summary>
		/// <param name="elapsedTime">Elapsed time since last draw operation.</param>
		/// <param name="bounds">The world boundary in which map items should be retrieved.</param>
		/// <param name="layer">The layer to draw.</param>
		protected virtual void DrawLayer(TimeSpan elapsedTime, Rect bounds, int layer)
		{
			//System.Diagnostics.Debug.WriteLine("=======================================================================");
			//System.Diagnostics.Debug.WriteLine("RenderLayer :: bounds = {0}, layer = {1}", bounds, layer);
			//System.Diagnostics.Debug.WriteLine("=======================================================================");

			// Only loop through the tiles within visible range.
			var items = this.Items.Where(i => (i.X <= bounds.X + bounds.Width && i.X + i.Width >= bounds.X)
					   && (i.Y <= bounds.Y + bounds.Height && i.Y + i.Height >= bounds.Y)
					   && i.Layer == layer).OrderBy(i => i.Z);

			foreach (var item in items)
			{
				var dx = 0D;
				var dy = 0D;
				var x = item.X;
				var y = item.Y;
				var w = item.Width;
				var h = item.Height;

				// Adjust the destination bounds for any tiles that bleed off the screen.
				if (x < bounds.X)
				{
					dx = bounds.X - x;
					w -= dx;
					x = bounds.X;
				}
				if (y < bounds.Y)
				{
					dy = bounds.Y - y;
					h -= dy;
					y = bounds.Y;
				}

				// TODO: Still not quite right, causes a flickering and funky sizes as the orgin moves to the edge.
				if ((x + w) > (bounds.Width)) w = bounds.Width - x;
				if ((y + h) > (bounds.Height)) h = bounds.Height - y;
				if (w < 0) w = 0;
				if (h < 0) h = 0;

				// Create the source rect, the area to capture within the source image that will be drawn.
				var srcRect = new Rect(dx, dy, w, h);
				System.Diagnostics.Debug.WriteLine("Source      :: x = {0}, y = {1}, w = {2}, h = {3}", x, y, w, h);

				// Apply map scale to position and width.
				x = (x - bounds.X) * MapScale;
				y = (y - bounds.Y) * MapScale;
				w = w * MapScale;
				h = h * MapScale;

				// Create the destination rectanlge, the actual position of the drawn image, offset to screen coordinates.
				var destRect = new Rect(x, y, w, h);
				System.Diagnostics.Debug.WriteLine("Destination :: x = {0}, y = {1}, w = {2}, h = {3}", x, y, w, h);
				//System.Diagnostics.Debug.WriteLine("=======================================================================");

				// Create the opacity color which sets the transparencey of the blitted image.
				var color = Color.FromArgb((byte)Convert.ToInt32(255 * item.Opacity), 255, 255, 255);

				// Raise the tile image request event.
				if (item.Tile.Image != null)
					_buffer.Blit(destRect, item.Tile.Image, srcRect, color, WriteableBitmapExtensions.BlendMode.AlphaBlend);

				if (this.SelectedItem != null && this.SelectedItem == item)
					_buffer.DrawRectangle(
						(int)destRect.X,
						(int)destRect.Y,
						(int)(destRect.X + destRect.Width),
						(int)(destRect.Y + destRect.Height),
						this.BorderColor);
			}
		}

		/// <summary>
		/// Draws the tile grid.
		/// </summary>
		/// <param name="bounds">The current world boundary.</param>
		protected virtual void DrawGrid(Rect bounds)
		{
			var x = (int)bounds.X;
			var y = (int)bounds.Y;

			var bx = x;
			var by = y;
			var bw = (int)bounds.Width;
			var bh = (int)bounds.Height;

			var gw = (int)GridSize.Width;
			var gh = (int)GridSize.Height;

			while (y < bh)
			{
				while (x < bw)
				{
					// Convert the world coordinates to grid coordinates.
					var gx = (x / gw) * gw;
					var gy = (y / gh) * gh;

					// Translate the grid coordinates into screen coordinates.
					var nx = gx - bx;
					var ny = gy - by;

					// Set the ending x and y positions based on grid size.
					var dx = nx + gw;
					var dy = ny + gh;

					// Adjust for scale.
					nx = (int)(nx * MapScale);
					ny = (int)(ny * MapScale);
					dx = (int)(dx * MapScale);
					dy = (int)(dy * MapScale);

					// Adjust the grid x and y to fit within the screen.
					if (nx < 0) nx = 0;
					if (ny < 0) ny = 0;

					// Ensure that grid lines do not go beyond the screen.
					if (dx >= _buffer.PixelWidth) dx = _buffer.PixelWidth - 1;
					if (dy >= _buffer.PixelHeight) dy = _buffer.PixelHeight - 1;

					_buffer.DrawRectangle(nx, ny, dx, dy, BorderColor);
					x += gw;
				}
				x = bx;
				y += gh;
			}
		}
		#endregion
	}
}
